(*****************************************************************************
 * Delphi Plug-in Framework (DPF)
 * Copyright (C) 2006-2007 Tao Yuan
 *
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the
 * License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
 * the specific language governing rights and limitations under the License.                                                               }
 *
 * The Initial Developer of the Original Code is Tao Yuan. Portions created by
 * Tao Yuan are Copyright (C) Tao Yuan (taoy.cn att gmail dott com)
 * All rights reserved.
 *****************************************************************************)
 
unit RdDefaultPluginsCollector;

interface

uses
  Classes,
  Rcl,
  RdPluginsCollector, RdPluginManager, RdStandardPluginLocation;

const
  PARAM_PLUGINS_REPOSITORIES =
      'dpf.plugin.boot.pluginsRepositories'; //$NON-NLS-1$
  PARAM_PLUGINS_LOCATIONS_DESCRIPTORS =
      'dpf.plugin.boot.pluginsLocationsDescriptors'; //$NON-NLS-1$


type
  TLocationsDescriptorHandler = class(TDefaultHandler)
  private
    resultData: IList;
    log: ILog;
  protected
    procedure startElement(const uri, localName, qName: SAXString;
      const atts: IAttributes);
  public
    constructor Create(aResult: IList);
  end;
  
  (**
   * Default implementation of plug-ins collector interface. Supported
   * configuration parameters are:
   * <dl>
   *   <dt>org.java.plugin.boot.pluginsRepositories</dt>
   *   <dd>Comma separated list of local plug-in repositories, given folders will
   *     be scanned for plug-ins. Default value is <code>./plugins</code>.</dd>
   *   <dt>org.java.plugin.boot.pluginsLocationsDescriptors</dt>
   *   <dd>Comma separated list of URLs for XML syntax files that describe
   *     available plug-in locations (see file syntax bellow). No default value
   *     provided.</dd>
   * </dl>
   * <p>
   * Given repositories are scanned recursively collecting all folders that
   * contain <code>plugin.xml</code> or <code>plugin-fragment.xml</code> and
   * <code>*.zip</code> and <code>*.jar</code> files.
   * </p>
   * <p>
   * Plug-ins locations descriptor is a simple XML syntax file that stores
   * locations of all available plug-in manifests and contexts (in terms of
   * {@link org.java.plugin.PluginManager.PluginLocation}). Here is an example:
   * <pre>&lt;plugins&gt;
   *   &lt;plugin
   *     manifest="http://localhost/myPlugins/plugin1/plugin.xml"
   *     context="http://localhost/myPlugins/plugin1/"/&gt;
   *   &lt;plugin
   *     manifest="http://localhost/myPlugins/plugin2/plugin.xml"
   *     context="http://localhost/myPlugins/plugin2/"/&gt;
   *   &lt;plugin
   *     manifest="http://www.plugins.com/repository/plugin1/plugin.xml"
   *     context="http://www.plugins.com/repository/plugin1/"/&gt;
   *   &lt;plugin
   *     manifest="http://www.plugins.com/repository/plugin1/plugin.xml"
   *     context="http://www.plugins.com/repository/plugin1/"/&gt;
   * &lt;/plugins&gt;</pre>
   * Using such simple descriptor you may, for example, publish plug-ins on a site
   * to make them available for clients without needing to download plug-ins
   * manually.
   * </p>
   * @version $Id: DefaultPluginsCollector.java,v 1.5 2006/04/09 14:42:47 ddimon Exp $
   *)

  TDefaultPluginsCollector = class(TInterfacedObject, IPluginsCollector)
  private
      procedure processDescriptor(url: string; result: IList);


  protected
      log: ILog;
      repositories,
      descriptors: IStrList;
      procedure processFile(aFile: string; result: IList);
    procedure processFolder(folder: string; result: IList);
    
  protected
  public
      constructor Create;
      destructor Destroy; override;
    function collectPluginLocations: ICollection;
    procedure configure(config: TExtendedProperties);
  end;

implementation

uses
  SysUtils,
  DPFLog;

{ TLocationsDescriptorHandler }

constructor TLocationsDescriptorHandler.Create(aResult: IList);
begin
  resultData := aResult;
  log := DPFLog.LogFactory.getLogger(Self);
end;

procedure TLocationsDescriptorHandler.startElement(
  const uri, localName, qName: SAXString; const atts: IAttributes);
var
  manifest,
  manifestUrl,
  context,
  contextUrl: string;
begin
  if (not Rcl.Str.IsEqual('plugin',qName)) then //$NON-NLS-1$
      Exit;
  
  manifest := atts.getValue('manifest'); //$NON-NLS-1$
  if (manifest = '') then
  begin
      log.warn('manifest attribute missing'); //$NON-NLS-1$
      Exit;
  end;

//  try
      manifestUrl := manifest;
//  } catch (MalformedURLException mue) {
//      log.warn('invalid manifest URL - ' + manifest, mue); //$NON-NLS-1$
//      return;
//  }
  context := atts.getValue('context'); //$NON-NLS-1$
  if (context = '') then
  begin
      log.warn('context attribute missing'); //$NON-NLS-1$
      Exit;
  end;

//  try
      contextUrl := context;
//  } catch (MalformedURLException mue) {
//      log.warn('invalid context URL - ' + context, mue); //$NON-NLS-1$
//      return;
//  }
  resultData.add(TStandardPluginLocation.Create(contextUrl, manifestUrl));
  log.debug('got plug-in location from descriptor, manifestUrl=' //$NON-NLS-1$
          + manifestUrl + ', contextURL=' + contextUrl); //$NON-NLS-1$
end;

constructor TDefaultPluginsCollector.Create;
begin
  log := DPFLog.LogFactory.getLogger(Self);
end;

destructor TDefaultPluginsCollector.Destroy;
begin
  inherited;
end;

{ TDefaultPluginsCollector }

function TDefaultPluginsCollector.collectPluginLocations: ICollection;
var
  it: IStrIterator;
  fileName: string;
begin
  Result := TLinkedList.Create;
  it := repositories.First;
  while it.HasNext do
  begin
    fileName := it.Next;
    if Rcl.FileUtils.IsDirectory(fileName) then
      processFolder(fileName, IList(result))
    else if Rcl.FileUtils.IsFile(fileName) then
      processFile(fileName, IList(result));
  end;

  it := descriptors.First;
  while it.HasNext do
  begin
    processDescriptor(it.next(), IList(result));
  end;
end;

procedure TDefaultPluginsCollector.configure(config: TExtendedProperties);
var
  pluginsRepositories,
  locationsDescriptors: string;
  token: string;
begin
  repositories := TStrLinkedList.Create;
  pluginsRepositories := config.getProperty(PARAM_PLUGINS_REPOSITORIES,
    '.' + Rcl.FileUtils.Separator + 'plugins');
  while pluginsRepositories <> '' do
  begin 
    token := Trim(Rcl.Str.StrToken(pluginsRepositories, ','));
    if token = '' then
      Continue;
    repositories.Add(Rcl.FileUtils.PathCanonicalize(token));  
  end;

  log.debug('found ' + IntToStr(repositories.size) //$NON-NLS-1$
          + ' local plug-ins repositories'); //$NON-NLS-1$

  descriptors := TStrLinkedList.Create;
  locationsDescriptors := config.getProperty(PARAM_PLUGINS_LOCATIONS_DESCRIPTORS,
    '');
  while locationsDescriptors <> '' do
  begin
    token := Trim(Rcl.Str.StrToken(locationsDescriptors, ','));
    if token = '' then
      Continue;
    descriptors.add(token);
  end;

  log.debug('found ' + IntToStr(descriptors.size) //$NON-NLS-1$
          + ' plug-ins locations descriptors'); //$NON-NLS-1$

end;

procedure TDefaultPluginsCollector.processDescriptor(url: string; result:
    IList);
var
  XMLReader: IXMLReader;
begin
  log.debug('processing plug-ins locations descriptor, URL=' + url); //$NON-NLS-1$
  try
    XMLReader := TXMLReaderFactory.GetReader;
    XMLReader.ContentHandler := TLocationsDescriptorHandler.Create(result);
    XMLReader.parse(url);
  except
    log.warn('failed processing plug-ins locations descriptor, URL=' //$NON-NLS-1$
        + url);
  end;
end;

{ Maybe, this procedure just deals with the package file, like .zip and so on } 
procedure TDefaultPluginsCollector.processFile(aFile: string; result: IList);
var
  manifestUrl,
  contextUrl: string;
  pluginLocation: TStandardPluginLocation;
begin
  log.debug('processing file - ' + aFile); //$NON-NLS-1$
  try
    pluginLocation := TStandardPluginLocation.Create(aFile);
    if pluginLocation <> nil then
      result.Add(pluginLocation);
  except
    log.warn('failed collecting plug-in file ' + aFile //$NON-NLS-1$
            + ', ignoring it'); //$NON-NLS-1$ 
  end;
end;

procedure TDefaultPluginsCollector.processFolder(folder: string; result: IList);
var
  manifestUrl,
  contextUrl: string;
//  fileEnum: IFileEnumerator;
  files: TStrings;
  I: Integer;
  fileName: string;
  pluginLocation: TStandardPluginLocation;
begin
  log.debug('processing folder - ' + folder); //$NON-NLS-1$
  try
    pluginLocation := TStandardPluginLocation.Create(folder);
    if pluginLocation <> nil then
    begin
      result.add(pluginLocation);
      Exit;
    end;
  except
    log.warn('failed collecting plug-in folder ' + folder //$NON-NLS-1$
            + ', ignoring it'); //$NON-NLS-1$
    exit;
  end;

//  manifestUrl := getManifestUrl(folder);
//  if (manifestUrl <> '') then
//  begin
//    contextUrl := folder;
//    result.add(TStandardPluginLocation.Create(contextUrl, manifestUrl));
//    Exit;
//  end;

  files := TStringList.Create;
  try
//    fileEnum.FillList(files);
    Rcl.FileUtils.ListFiles(folder, files);
    for I := 0 to files.Count - 1 do
    begin
      fileName := files.Strings[I];
      if Rcl.FileUtils.IsDirectory(fileName) then
        processFolder(fileName, result)
      else if Rcl.FileUtils.IsFile(fileName) then
        processFile(fileName, result);
    end;
  finally
    files.Free;
  end;

end;

end.
